﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using Vortex.Debugging;
using Vortex.IO;
using System.Xml;
using Vortex.Utils;
using Vortex.SceneFramework.CSS;

namespace Vortex.SceneFramework {

	/// <summary>
	/// Loader of domain layout from XML layout definition.
	/// </summary>
	public class SceneLayoutLoader {
		private readonly static Dictionary<string, Type> _sceneElementNameMapping = new Dictionary<string, Type>();
		private readonly static Assembly THIS_ASSEMBLY = Assembly.GetExecutingAssembly();
		private readonly static Type ELEMENT_TYPE = typeof(Node);
		private readonly static Type SCENE_TYPE = typeof(DomainNode);
		private readonly static string STYLE_TAG = "Style";
		private readonly static string STYLEREF_TAG = "StyleRef";
		private readonly static string EVENT_ATTRIBUTE_PREFIX = "e-";
		private readonly static string STYLE_ATTRIBUTE = "Style";
		private readonly static char[] STYLE_SEPARATOR = new char[] { ';' };
		private readonly static char STYLE_PROPERTY_VALUE_SEPARATOR = ':';

		private DomainNode _scene;
		private Type _sceneType;

		static SceneLayoutLoader() {
			Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
			foreach (Assembly assembly in loadedAssemblies) {
				ScanAssembly(assembly);
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="SceneLayoutLoader"/> class targetting specified domain.
		/// </summary>
		/// <param name="domain">The target domain to initialize.</param>
		public SceneLayoutLoader(DomainNode scene) {
			_scene = scene;
			_sceneType = scene.GetType();
		}

		#region Element Mapping Management

		/// <summary>
		/// Scans the specified assembly to find graph elements. Graph element is Non-Abstract class which inherits <see cref="Element"/> type but not inherits <see cref="Domain"/>.
		/// </summary>
		/// <param name="assembly">The assembly to scan.</param>
		public static void ScanAssembly(Assembly assembly) {
			if (null == assembly) throw new ArgumentNullException("assembly");

			if (assembly == THIS_ASSEMBLY || CheckRefersVortexAssembly(assembly)) {
				using (Log.TraceUp("Scanning assembly '{0}' for SceneGraph elements...", assembly.FullName)) {
					Type[] exportedType = assembly.GetTypes();
					foreach (Type type in exportedType) {
						//looking for non-abstract class which inherits Element type
						if (type.IsClass && !type.IsAbstract && ELEMENT_TYPE.IsAssignableFrom(type) && !SCENE_TYPE.IsAssignableFrom(type)) {
							RegisterElementType(type);
							Log.Info("Found element of type '{0}', short name: '{1}'", type.FullName, type.Name);
						}
					}
				}
			}
		}

		public static void RegisterElementType(Type type) {
			if (null == type) throw new ArgumentNullException("type");

			string elementShortName = type.Name;

			//check is there existing element name
			Type existingType;
			if (_sceneElementNameMapping.TryGetValue(elementShortName, out existingType)) {
				if (existingType != type) {
					Log.Warning("Scene Element of type '{0}' hides another type '{1}' short name '{2}'", type.FullName, existingType.FullName, elementShortName);
				}
				_sceneElementNameMapping.Remove(elementShortName);
				_sceneElementNameMapping.Remove(type.FullName);
			}
			//register new elements
			_sceneElementNameMapping.Add(elementShortName, type);
			_sceneElementNameMapping.Add(type.FullName, type);
		}

		/// <summary>
		/// Checks does the specified assembly refers Vortex assembly.
		/// </summary>
		/// <param name="assembly">The assembly to check.</param>
		/// <returns><c>true</c> if specified assembly refers Vortex assembly; otherwise <c>false</c></returns>
		private static bool CheckRefersVortexAssembly(Assembly assembly) {
			AssemblyName thisName = THIS_ASSEMBLY.GetName();
			AssemblyName[] names = assembly.GetReferencedAssemblies();
			foreach (AssemblyName name in names) {
				if (name.FullName.Equals(thisName.FullName)) {
					return true;
				}
			}
			return false;
		}

		/// <summary>
		/// Finds the type of the element by its name.
		/// </summary>
		/// <param name="componentName">Name of the element.</param>
		/// <returns><see cref="System.Type"/> if element mapping exists; otherwise <c>null</c></returns>
		private static Type FindElementType(string elementName) {
			Type elementType;
			_sceneElementNameMapping.TryGetValue(elementName, out elementType);
			return elementType;
		}

		#endregion

		/// <summary>
		/// Loads the XML layout definition from specified file path.
		/// </summary>
		/// <param name="xmlFilePath">The XML definition file path.</param>
		public void Load(string xmlFilePath) {
			if (null == xmlFilePath) throw new ArgumentNullException("xmlFilePath");

			using (Log.TraceUp("Loading scene layout from file '{0}'", xmlFilePath)) {
				LoadXml(new ResourceFileInfo(xmlFilePath).GetContentAsText());
			}
		}

		/// <summary>
		/// Loads the XML layout definition from the XML string.
		/// </summary>
		/// <param name="xmlLayoutDefinition">The XML layout definition string.</param>
		public void LoadXml(string xmlLayoutDefinition) {
			if (null == xmlLayoutDefinition) throw new ArgumentNullException("xmlLayoutDefinition");

			using (Log.TraceUp("Parsing XML scene layout definition")) {
				Log.Details(xmlLayoutDefinition);

				PerfTime begin = PerfTime.Now;
				//create and load document
				XmlDocument document = new XmlDocument();
				document.LoadXml(xmlLayoutDefinition);
				//process document
				ProcessDocument(document);

				PerfTimeSpan layoutLoadTime = PerfTime.Now - begin;
				Log.Trace("Layout load time: {0:F1} ms", layoutLoadTime.Milliseconds);

			}
		}

		/// <summary>
		/// Processes the XML layout document.
		/// </summary>
		/// <param name="document">The XML document.</param>
		private void ProcessDocument(XmlDocument document) {
			XmlNodeList sceneNodeList = document.SelectNodes("Scene");
			if (sceneNodeList.Count == 1) {
				XmlNode sceneNode = sceneNodeList[0];
				//process domain attributes first
				ProcessElementNodeProperties(_scene, sceneNode);
				//then lets fill domain with content
				ProcessNodeContent(_scene, sceneNode.ChildNodes);
			} else {
				throw new SceneLayoutXmlException("Scene layout XML should have single root node 'Scene'");
			}
		}

		/// <summary>
		/// Processes the content of the node.
		/// </summary>
		/// <param name="parentElement">The parent element - container.</param>
		/// <param name="elementInnerNodes">The element's inner nodes.</param>
		private void ProcessNodeContent(Node parentElement, XmlNodeList elementInnerNodes) {
			List<Node> childNodes = new List<Node>();
			foreach (XmlNode node in elementInnerNodes) {
				if (node.NodeType == XmlNodeType.Element) {
					if (node.Name == STYLE_TAG) {
						ProcessEmbeddedStyleSheetNode(node);
					} else if (node.Name == STYLEREF_TAG) {
						ProcessStyleRefNode(node);
					} else {
						childNodes.Add(ProcessNode(node));
					}
				}
			}
			//cool now we can add all of child elements into parent containers
			foreach (Node childNode in childNodes) {
				parentElement.AddChild(childNode);
			}
		}

		/// <summary>
		/// Processes the style reference node.
		/// </summary>
		/// <param name="node">The xml node.</param>
		private void ProcessStyleRefNode(XmlNode node) {
			if (!node.HasChildNodes) {
				XmlAttribute fileAttribute = node.Attributes["File"];
				if (null != fileAttribute) {
					//check up with relative and 
					string fileName = fileAttribute.Value;
					if (PathUtils.IsPathContextRooted(fileName)) {
						fileName = PathUtils.EscapeContextRootedPath(fileName);
					} else if (!PathUtils.IsPathRooted(fileName)) {						
						fileName = _scene.BaseDir + @"\" + fileName;
					}
					//parse CSS file
					_scene.StyleSheet.ParseCSSFile(fileName);
					return;
				}
			}
			//log warning of wrong style
			Log.Warning("Invalid style reference tag");
			using (Log.Up()) {
				Log.Details(node.OuterXml);
			}
		}

		/// <summary>
		/// Processes the embedded style sheet node.
		/// </summary>
		/// <param name="node">The stylesheet node.</param>
		private void ProcessEmbeddedStyleSheetNode(XmlNode node) {
			_scene.StyleSheet.ParseCSS(node.InnerXml);
		}

		/// <summary>
		/// Processes the domain element node definition.
		/// </summary>
		/// <param name="node">The node to process.</param>
		/// <returns><see cref="Element"/> instance resolved from XML node instance.</returns>
		private Node ProcessNode(XmlNode node) {
			Type nodeElementType = FindElementType(node.Name);
			if (null == nodeElementType) {
				throw new SceneLayoutXmlException("Unable to resolve node '{0}' into entity element", node.Name);
			}
			
			Node currentElement = (Node)Activator.CreateInstance(nodeElementType);

			//process element properties (as attributes)
			ProcessElementNodeProperties(currentElement, node);

			//independently we may process element content
			XmlNodeList childNodes = node.ChildNodes;
			if (childNodes.Count >= 1) {
				ProcessNodeContent(currentElement, childNodes);
			}

			//try to set automatically element reference via reflection
			//NOTE: No more live domain binding... only elements marked with attributes will be set
			//TryBindToSceneFieldOrProperty(currentElement);

			return currentElement;
		}

		/// <summary>
		/// Tries to bind the element to domain field or property.
		/// </summary>
		/// <param name="currentElement">The element to bind.</param>
		private void TryBindToSceneFieldOrProperty(Node currentElement) {
			BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
			if (!string.IsNullOrEmpty(currentElement.Id)) {
				PropertyInfo propertyInfo = _sceneType.GetProperty(currentElement.Id, bindingFlags);
				if (propertyInfo != null && propertyInfo.PropertyType.IsInstanceOfType(currentElement)) {
					propertyInfo.SetValue(_scene, currentElement, new object[] {});
				} else {
					FieldInfo fieldInfo = _sceneType.GetField(currentElement.Id, bindingFlags);
					if (fieldInfo != null && fieldInfo.FieldType.IsInstanceOfType(currentElement)) {
						fieldInfo.SetValue(_scene, currentElement);
					}
				}
			}
		}

		/// <summary>
		/// Processes the attribute collection and use it as public property initializer. Property name should match attribute.
		/// </summary>
		/// <param name="targetElement">The target element.</param>
		/// <param name="xmlAttributeCollection">The XML attribute collection with properties.</param>
		private void ProcessElementNodeProperties(Node targetElement, XmlNode node) {
			//process each attribute
			foreach (XmlAttribute attribute in node.Attributes) {
				ProcessElementAttribute(targetElement, attribute);
			}

			//also if node text present lets set it as text property :)
			ITextNode textAwareElement = targetElement as ITextNode;
			if (textAwareElement != null) {
				string trimmedText = node.InnerText.Trim();
				if (trimmedText.Length > 0) {
					textAwareElement.Text = trimmedText.Replace("\t", "").Replace("\r", "");
				}
			}
		}

		private void ProcessElementAttribute(Node targetElement, XmlAttribute attribute) {
			string propertyName = attribute.Name;
			if (STYLE_ATTRIBUTE == propertyName) {
				ParseStyleString(targetElement, attribute.Value);
			} else if (propertyName.StartsWith(EVENT_ATTRIBUTE_PREFIX)) {
				ProcessEventAttribute(targetElement, attribute);
			} else {
				ResolveElementProperty(targetElement, attribute.Name, attribute.Value);
			}
		}

		private void ResolveElementProperty(Node targetElement, string propertyName, string propertyValue) {
			IPropertyResolver resolver = _scene.GetPropertyResolver();
			Type elementType = targetElement.GetType();

			//find public property exactly by attribute name
			PropertyInfo propertyInfo = elementType.GetProperty(propertyName);
			//resolve property
			object value = resolver.ResolveProperty(propertyName, propertyInfo.PropertyType, propertyValue);
			//remove \r characters
			if (null != value && propertyInfo.PropertyType == typeof(string)) {
				value = ((string)value).Replace(@"\n", "\n");
			}
			//set value through the reflection
			propertyInfo.SetValue(targetElement, value, new object[] { });
		}

		/// <summary>
		/// Processes the event attribute. This method binds string method name with existing method
		/// </summary>
		/// <param name="targetElement">The target element.</param>
		/// <param name="attribute">The attribute.</param>
		private void ProcessEventAttribute(Node targetElement, XmlAttribute attribute) {
			string propertyName = attribute.Name;
			Type elementType = targetElement.GetType();
			string eventName = propertyName.Substring(EVENT_ATTRIBUTE_PREFIX.Length);
			string methodName = attribute.Value;

			System.Reflection.EventInfo eventInfo = elementType.GetEvent(eventName);
			if (eventInfo == null) {
				throw new SceneLayoutXmlException("Unable to find event '{0}' specified by event property {1}", eventName, propertyName);
			}

			MethodInfo methodInfo = _sceneType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (methodInfo == null) {
				throw new SceneLayoutXmlException("Unable to find method '{0}' specified by event property {1}", methodName, propertyName);
			}

			//create delegate
			Delegate d = Delegate.CreateDelegate(eventInfo.EventHandlerType, _scene, methodInfo, false);
			if (null == d) {
				//we don't ignore wrong delegates, ok?
				throw new SceneLayoutXmlException(
					"Unable to bind instance method '{0}' ('{1}') with event '{2}' of type '{3}'. Method/delegate signature mismatch",
					methodInfo.Name, _sceneType.FullName, eventInfo.Name, eventInfo.EventHandlerType.FullName
				);
			}

			//and attach delegate as event at the end!
			eventInfo.AddEventHandler(targetElement, d);
		}

		/// <summary>
		/// Parses the inline style string with simple algorithm:
		/// 1) Separator between properties is ";", it can't be a part of value
		/// 2) First occurence of ":" means end of property name.
		/// 3) Everything after first ":" is value
		/// </summary>
		/// <param name="targetElement">The target element.</param>
		/// <param name="styleString">The style string to parse.</param>
		private void ParseStyleString(Node targetElement, string styleString) {
			foreach (string element in styleString.Split(STYLE_SEPARATOR, StringSplitOptions.RemoveEmptyEntries)) {
				string styleExpression = element.Trim();
				int splitIndex = element.IndexOf(STYLE_PROPERTY_VALUE_SEPARATOR);
				if (splitIndex > 0 && splitIndex < (styleExpression.Length - 1) ) {
					string propertyName = element.Substring(0, splitIndex).Trim();
					string propertyValue = element.Substring(splitIndex + 1).Trim();

					StyleKey property = new StyleKey(propertyName);
					targetElement.Style[property] = _scene.GetPropertyResolver().ResolveProperty(property.Name, property.Type, propertyValue);
					continue;
				}

				//something was wrong, lets notify about it
				Log.Warning("Unable to recognize style expression '{0}'", styleExpression);
			}
		}
	}

	#region Exceptions

	/// <summary>
	/// Defines exceptional situation during parsing of domain layout XML
	/// </summary>
	public class SceneLayoutXmlException : Exception {

		public SceneLayoutXmlException(string format, params object[] args)
			: base (string.Format(format, args)) {
		}

		public SceneLayoutXmlException(Exception cause, string format, params object[] args)
			: base(string.Format(format, args), cause) {
		}
	}

	#endregion
}
